package engine.io;

import java.util.List;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import primitives.Box;
import primitives.Light;
import primitives.Patch;
import primitives.Room;
import primitives.Shape;
import primitives.Sphere;

import com.google.common.collect.Lists;

public class ShapeReader extends Reader {

	private List<Patch> leaves;
	
	private final String[] PRIMITIVES = {"room","box","sphere","light"};
	private final String[] BOX_DIMENSIONS = {"width", "height", "length"};
	private final String[] SPHERE_DIMENSIONS = {"radius"};
	private final String[] TRANS = {"xTranslate", "yTranslate", "zTranslate"};
	private final String[] ROTS = {"xRotate", "yRotate", "zRotate"};
	private final int TIMES_TO_SUBDIVIDE = 2;
	private final int LIGHT_SUBDIVISIONS = 0;
	
	/*
	 * Gets a list of primitives defined in XML document.
	 * @param doc The XML document that defines the primitives.
	 *   Each primitive is defined by tag <shape /> and must contain at least
	 *   <type /> tag.
	 * @return List<Shape> of primitives
	 */
	public List<Shape> getShapes(Document doc) {
		leaves = Lists.newArrayList();
		List<Shape> shapes = Lists.newArrayList();
		for(String type : PRIMITIVES) {
			NodeList list = doc.getElementsByTagName(type);
			for(int node = 0; node < list.getLength(); node++) {
				Element shapeXML = (Element) list.item(node);
				Shape shape = parseShape(shapeXML, type);
				if(shape != null) {
					subdivideShape(shape);
				}
				shapes.add(shape);
			}
		}
		return shapes;
	}
	
	/*
	 * Creates a shape object from XML.
	 * @node the DOM node that represents this shape
	 * @return Shape the Shape object defined by the XML
	 */
	private Shape parseShape(Element shapeXML, String type) {
		Shape shape = parseShapeType(type);
		if(shape == null) {
			return null;
		}
		double[] dimensions = parseDimensions(shape, shapeXML);
		if(dimensions == null) {
			return null;
		}
		double[] rotations = parseList(shapeXML, "rotate", ROTS);
		double[] translations = parseList(shapeXML, "translate", TRANS);
		setTransformations(shape, dimensions, translations, rotations);
		return shape;
	}
	
	/*
	 * Parses type of Shape from XML.
	 * @param type the text content of type in shape's XML
	 * @return Instance of Shape type
	 */
	private Shape parseShapeType(String type) {
		Shape shape;
		if(type.equalsIgnoreCase("box")) {
			shape = new Box();
		} else if(type.equalsIgnoreCase("sphere")) {
			shape = new Sphere();
		} else if(type.equalsIgnoreCase("room")) {
			shape = new Room();
		} else if(type.equalsIgnoreCase("light")) {
			shape = new Light();
		} else {
			System.err.println("Unknown shape");
			return null;
		}
		return shape;
	}
	
	private void subdivideShape(Shape shape) {
		shape.initPatches();
		shape.initLight();
    	if(shape instanceof Light) {
    		leaves.addAll(shape.subdividePatches(LIGHT_SUBDIVISIONS));
    	} else {
    		leaves.addAll(shape.subdividePatches(TIMES_TO_SUBDIVIDE));
    	}
	}
	
	/*
	 * Parse Shape's dimensions using polymorphism.
	 */
    private double[] parseList(Element shapeXML, String listID, 
                String[] listElements) {
        NodeList typeList = shapeXML.getElementsByTagName(listID);
        Element list = (Element)typeList.item(0);
        double[] values = new double[listElements.length];
        for(int i = 0; i < listElements.length; i++) {
            if(list == null) {
	            values[i] = 0.0;
	            continue;
            }
            Node listItem = list.getElementsByTagName(listElements[i]).item(0);
            if(listItem == null) {
                if(listID == "dimensions") {
                    System.err.println("Shape does not contain required element "
                                    + listElements[i]);
                    return null;
                } else {
                    values[i] = 0.0;
                    continue;
                }
            }
            String strValue = listItem.getTextContent();
            try {
                double value = Double.parseDouble(strValue);
                values[i] = value;
            } catch(NumberFormatException e) {
                    System.err.println("Invalid value for element");
                    return null;
            }
        }
        return values;
    }
	
    private double[] parseDimensions(Shape shape, Element shapeXML) {
    	if(shape instanceof Box) {
    		return parseList(shapeXML, "dimensions", BOX_DIMENSIONS);
    	} else if(shape instanceof Sphere) {
    		return parseList(shapeXML, "dimensions", SPHERE_DIMENSIONS);
    	} else {
    		return null;
    	}
    }
    
	private void setTransformations(Shape shape, double[] scales, 
			double[] translations, double[] rotations) {
		shape.setInverseTransformMatrix(scales[0], scales[1], scales[2],
				translations[0], translations[1], translations[2], 
				Math.toRadians(rotations[0]), Math.toRadians(rotations[1]), 
				Math.toRadians(rotations[2]));
		shape.setTransformMatrix(scales[0], scales[1], scales[2],
				translations[0], translations[1], translations[2], 
				Math.toRadians(rotations[0]), Math.toRadians(rotations[1]), 
				Math.toRadians(rotations[2]));
	}
	
	public List<Patch> getPatches() {
		return this.leaves;
	}
	
}
